gqlgen でGraphQLサーバーを半年間運用してみた感想
まとめ
schemaからコードを生成してくれる
生成されたコードも型安全
スキーマの設計
query/mutation 名
input
そのうち入るかも?
アクセス制御
gqlgenは1.0に向けて活発に開発されているので、期待している contributeする機会があればやりたい
---
以下記事
こんにちは、ebiken です。
自分は今スタートアップでバックエンドエンジニアとして、gqlgenを使ったGraphQLサーバーを運用しています。運用し始めて半年ほど経ったので感想とかを色々書いていきます。 まずは軽くGraphQLとgqlgenについて紹介します。
GraphQL
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
仕様や実装などについて書かれているおすすめの記事を載せておきます。
gqlgen
gqlgen is a Go library for building GraphQL servers without any fuss.
gqlgenはスキーマファースト、型安全、コード生成という特徴を持ったGoのGraphQLサーバーライブラリです。
---
GraphQL, gqlgenの特徴についてそれぞれ運用してきた感想を書いていきます。
スキーマファースト
gqlgenはGraphQLのスキーマからコードを生成し、作成されたresolverのinterfaceを満たすように実装するというフローになります。そのようにschemaを最初に設計し、それをもとにサーバー/クライアントの実装を進めることをスキーマファースト開発といいます。
開発チームの背景としてリモート開発がメイン、サーバー/クライアントを実装する人が別というのがあったため、スキーマファーストで開発を進めることは非常に重要でした。
具体的なフローはこんな感じです。
1. スキーマを書く
code:graphql
query {
...
users: UserConnection!
}
2. 新しいスキーマで go generate を行い、interfaceを満たす最低限の resolver を書く(具体的な実装はしない)
code:go
func (r *queryResolver) Users(ctx context.Context) (*UserConnection, error) {
panic("not implemented")
}
code:sh
$ apollo schema:download <schema.json ファイルのパス> \
--endpoint <GraphQLサーバーのエンドポイント> \
--header <認証用のヘッダー>
4. サーバー/フロントの実装を進める
このようなフローになっているので、実装後にサーバー/クライアント間の擦り合せ(パラメータが違う、型が違うなど)をする必要がなくなりコミュニケーションコストを減らすことができています。
スキーマファースト開発について参考にした記事を貼っておきます。
n+1問題
GraphQLを使う際に発生する問題としてn+1問題が挙げられます。GraphQLはresolverを別々に実行していくため、同じリソースに対しても都度DB等へのリクエストを行ってしまうのは良くないです。
n+1問題に対してgqlgenは dataloaden を使用して実装することを推奨しているのですが、こちらも go generate ベースで型安全なバッチ処理を簡単に実装する事ができるので凄く良いです。 pagination
しかし仕様に記述されているアルゴリズムはリストを全部取ってきて処理するという形式のため、パフォーマンス等を考慮するとRDBを使用している場合そのまま実装するわけにはいきません。そのため、first/last/before/after の指定に合わせてクエリを組み立てるよう自前で実装しました。
実装にはこちらで議論されているものを参考にしました。
このpaginationの実装に関してはgqlgenにpluginとして入れられるようになりそうな話が出ていました。
スキーマの設計
GraphQLの公式ページにはスキーマの書き方や機能について書いてあるものの、それを実際にどう使っていくのかといった情報はまだ少ないです。そのため最初はスキーマの設計に苦労しました。何度かスキーマのリファクタリングをしています。
実際に使用しているスキーマの設計はこんな感じです。
ID はglobalでユニークな値を指定する
引数には XXInput という名前で input オブジェクトを使用する
code:graphql
# schema.graphql
mutation {
updateUser(input: UpdateUserInput!): User!
}
# user.graphql
input UpdateUserInput {
name: String!
}
オブジェクトのリストを返したい場合はtypeを Connection として定義する
pagination用のInputは PaginationInput として定義し引数に使用する
code:graphql
# schema.graphql
query {
users(page: PaginationInput!): UserConnection!
}
# page.graphql
input PaginationInput {
first: Int
last: Int
before: String
after: String
}
Connection, Edge, Node interfaceを定義しpaginationものはそれを継承する
code:graphql
# user.graphql
type User implements Node {
identifier: ID!
name: String!
}
type UserEdge implements Edge {
cursor: String!
node: User!
}
type UserConnection implements Connection {
pageInfo: PageInfo!
}
# page.graphql
type PageInfo {
startCursor: String!
endCursor: String!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
interface Connection {
pageInfo: PageInfo!
}
interface Edge {
cursor: String!
node: Node!
}
interface Node {
identifier: ID!
}
orderが複数あるクエリの場合、 orderBy inputを指定する
code:graphql
# schema.graphql
query {
posts(page: PaginationInput!, orderBy: PostOrder!): PostConnection!
}
# post.graphql
input PostOrder {
field: PostOrderFields!
}
enum PostOrderFields {
CREATED_AT
POPULARITY
}
このあたりを参考にしています。特に9月の技術書展7でvvakameさんが執筆されたGraphQLスキーマ設計ガイドは非常に参考になりました。
アクセス制御
管理画面からのみ一部のquery/mutationに対してアクセスを許可したいという要件があったため、directive を使用しました。
一部のRoleからのみのアクセスを許可するのを以下のようにして実現しています。
code:graphql
# directive の定義
directive @hasRole(role: Role!) on FIELD_DEFINITION
enum Role {
ADMIN
}
# directiveの使用
mutation {
updateUserByAdmin(input: UpdateUserByAdminInput!): User! @hasRole(role: ADMIN)
}
上記に書いた以外にもGraphQLを使った開発には多くのメリットがあり、特にDX(Developer Experience)が良いと思っています。
そのような高いDXがあり、速い開発スピードも出せているので、GraphQL+gqlgenを選んで良かったと思っています。